package cn.ycc1.functionlibrary.reflection;

/**
 * Invoking Methods
 * @author ycc
 * @date 2025/3/9
 * A Method object let you get more information on the corresponding method: its type and its modifiers, the types and names of
 * its parameters, and enables you to invoke it on a given object, passing the arguments you need. This section also covers how
 * you can discover methods in a class.
 */
public class InvokingMethods {
    /**
     * Locating Methods
     * There are two categories of methods provided in Class for accessing methods.
     *
     * First, you can look for a specific method. These methods suppose that you have a name for the method you are looking for,
     * and the type of its parameters.
     * Second, you can look for all the methods that are declared in this class, or for the methods declared in this class,
     * and all its super classes, up to the Object class. In the first case, you will get all the methods: public, protected,
     * default (package) access, and private. In the second case, you will get only the public methods.
     * The following tables provide a summary of all the member-locating methods and their characteristics.
     *
     * Class API	Array of methods?	Inherited methods?	Private methods?
     * getDeclaredMethod(String, Object...)	no	no	yes
     * getMethod(String, Object...)	no	yes	no
     * getDeclaredMethods()	yes	no	yes
     * getMethods()	yes	yes	no
     * Let us see this in action. Suppose that you have the following class.
     *
     * public class User {
     *     private final String name;
     *
     *     public User(String name) {
     *         validate(name);
     *         this.name = name;
     *     }
     *
     *     private void validate(String name) {
     *         Objects.requireNonNull(name);
     *     }
     *
     *     public String getName() {
     *         return name;
     *     }
     *
     *    @Override
     *    public String toString() {
     *        return "User[" + "name=" + name + "]";
     *    }
     * }
     * First, let us run the following code, that gets all the public methods from the User class and all of its super classes.
     *
     * Class<?> c = User.class;
     * Method[] methods = c.getMethods();
     * for (Method method : methods) {
     *     System.out.println("method = " + method);
     * }
     * Running the previous code prints the following. As you can see, there are the two methods from User: getName() and
     * toString(), and the methods from the Object class, apart from the toString() method, which is defined in the User class.
     * If you remove the toString() method from the User class, then you will see the toString() from the Object class in this
     * list.
     *
     * method = public java.lang.String org.devjava.User.getName()
     * method = public java.lang.String org.devjava.User.toString()
     * method = public boolean java.lang.Object.equals(java.lang.Object)
     * method = public native int java.lang.Object.hashCode()
     * method = public final native java.lang.Class java.lang.Object.getClass()
     * method = public final native void java.lang.Object.notify()
     * method = public final native void java.lang.Object.notifyAll()
     * method = public final void java.lang.Object.wait(long) throws java.lang.InterruptedException
     * method = public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
     * method = public final void java.lang.Object.wait() throws java.lang.InterruptedException
     * Let us now run the following code, that is calling Class.getDeclaredMethods() to get all the methods from the User class.
     *
     * Class<?> c = User.class;
     * Method[] methods = c.getDeclaredMethods();
     * for (Method method : methods) {
     *     System.out.println("method = " + method);
     * }
     * Running the previous code prints the following. This time, only the methods declared in the User class are present,
     * including the private methods.
     *
     * method = public java.lang.String org.devjava.User.getName()
     * method = public java.lang.String org.devjava.User.toString()
     * method = private void org.devjava.User.validate(java.lang.String)
     */

    /**
     * Obtaining the Return Type of a Method
     * The return type of a method is modeled in the same way as the type of a Field. The Method.getReturnType() method gives
     * you the type as a Class object, and the Method.getGenericReturnType() returns a Type object from which you can get
     * information about the generic type.
     *
     * Let us see these two methods in action on the List interface.
     *
     * First, you can get a reference on the method List.get(int). This method returns the element of the list with the index
     * you passed as an argument. The return type of this method is the parameter type of the list you declared.
     *
     * Note that we used the class int.class to denote the int type of the parameter that the List.get(index) method takes.
     * Using the Integer.class type would have thrown a NoSuchMethodException.
     *
     * Class<?> listClass = List.class;
     * Method getWithIndex = listClass.getMethod("get", int.class);
     *
     * Class<?> returnType = getWithIndex.getReturnType();
     * System.out.println("Return type = " + returnType);
     *
     * Type genericReturnType = getWithIndex.getGenericReturnType();
     * System.out.println("Generic return type = " + genericReturnType);
     * Running the previous example gives you the following.
     *
     * Return type = class java.lang.Object
     * Generic return type = E
     * As you can see, the Method.getReturnType() gives you the raw type that this method returned, whereas the
     * Method.getGenericReturnType() gives you the information that it is in fact the parameter type of the List interface.
     *
     * Let us test it with another method: List.of().
     *
     * Class<?> listClass = List.class;
     * Method listOf = listClass.getMethod("of", Object.class);
     *
     * Class<?> returnType = listOf.getReturnType();
     * System.out.println("Return type = " + returnType);
     *
     * Type genericReturnType = listOf.getGenericReturnType();
     * System.out.println("Generic return type = " + genericReturnType);
     * Running this example gives you the following.
     *
     * Return type = interface java.util.List
     * Generic return type = java.util.List<E>
     */

    /**
     * Obtaining the Parameters of a Method
     * The Method class gives you access to the parameters of a given method, including their names.
     *
     * Obtaining the Names of Method Parameters
     * You can obtain the names of the formal parameters of any method or constructor (that are covered in the next section)
     * with the method getParameters(). The classes Method and Constructor extend the class Executable and therefore inherit
     * this method. However, .class files do not store formal parameter names by default. This is because many tools that
     * produce and consume class files may not expect the larger static and dynamic footprint of .class files that contain
     * parameter names. In particular, these tools would have to handle larger .class files, and the
     * Java Virtual Machine (JVM) would use more memory. In addition, some parameter names, such as secret or password,
     * may expose information about security-sensitive methods.
     *
     * To store formal parameter names in a particular .class file, and thus enable the Reflection API to retrieve formal
     * parameter names, compile the source file with the -parameters option to the javac compiler. You can find more information
     * the javac compiler in this section.
     *
     * Obtaining the Return Type of a Method
     * The return type of a method is modeled in the same way as the type of a Field. The Method.getReturnType() method gives
     * you the type as a Class object, and the Method.getGenericReturnType() returns a Type from which you can get information
     * about the generic type.
     */

    /**
     * Obtaining the Exceptions of a Method
     * The Method class gives you access to the declared exceptions of a method. These exceptions are returned in an array,
     * which can be of two types: an array of Class instances, or an array of Type instances. As usual the Type object can give
     * you information about a parameter type.
     *
     * Let us discover the numerous exceptions thrown by the Constructor.newInstance() method, that is part of the Reflection
     * API. Note how you can denote an array of Object type, with the Object[].class syntax.
     *
     * Class<?> constructorClass = Constructor.class;
     * Method newInstance = constructorClass.getMethod("newInstance", Object[].class);
     *
     * var exceptionTypes = newInstance.getExceptionTypes();
     * System.out.println("exceptionTypes:");
     * for (var exceptionType : exceptionTypes) {
     *     System.out.println("   exceptionType = " + exceptionType);
     * }
     * Running this example produces the following:
     *
     * Exception types:
     *    Exception type = class java.lang.InstantiationException
     *    Exception type = class java.lang.IllegalAccessException
     *    Exception type = class java.lang.IllegalArgumentException
     *    Exception type = class java.lang.reflect.InvocationTargetException
     * You can run the same code using the other pattern, that returns an array of Type objects. Note that in that case, you
     * are calling Method.getGenericExceptionTypes().
     *
     * Class<?> constructorClass = Constructor.class;
     * Method newInstance = constructorClass.getMethod("newInstance", Object[].class);
     * var exceptionTypes = newInstance.getGenericExceptionTypes();
     * System.out.println("Exception types:");
     * for (var exceptionType : exceptionTypes) {
     *         System.out.println("   Exception type = " + exceptionType);
     * }
     * The result printed on the console is the same.
     *
     * Exception types:
     *    Exception type = class java.lang.InstantiationException
     *    Exception type = class java.lang.IllegalAccessException
     *    Exception type = class java.lang.IllegalArgumentException
     *    Exception type = class java.lang.reflect.InvocationTargetException
     */

    /**
     * Retrieving Method Modifiers
     * The modifiers of a method work in the same way as the modifiers of a class or a field. The method Method.getModifiers()
     * returns an int in which each bit represent a single modifier. For instance, if the bit 0 is set, it means that this
     * method is public. If the bit 3 is set, then this method is static.
     *
     * You can read the modifiers of the String.join() method with the following code.
     *
     * Class<?> stringClass = String.class;
     * Method join = stringClass.getMethod("join", CharSequence.class, Iterable.class);
     *
     * int modifiers = join.getModifiers();
     *
     * System.out.println("is static? " + Modifier.isStatic(modifiers));
     * System.out.println("is public? " + Modifier.isPublic(modifiers));
     * This code prints the following on the console.
     *
     * is static? true
     * is public? true
     * You can also use the access flags, covered in this section
     *
     * Class<?> stringClass = String.class;
     * Method join = stringClass.getMethod("join", CharSequence.class, Iterable.class);
     * Set<AccessFlag> accessFlags = join.accessFlags();
     * System.out.println("Access flags: " + accessFlags);
     * This code prints the following on the console.
     *
     * Access flags: [PUBLIC, STATIC]
     */

    /**
     * Invoking a Method
     * So far you learnt that a method can be modeled by an object instance of the Method class, and that you can use this
     * object to get information on the method it models, including its name, parameters, return type, and the exceptions this
     * method may declare.
     *
     * Using this object, you can also invoke this method, passing it arguments and getting its result. If it throws exceptions,
     * you can also catch them and analyze them. This feature of the Method class is widely used in many frameworks, because it
     * allows you to execute a method from elements that are discoverable.
     *
     * A method can be invoked using its invoke() method, that takes two parameters.
     *
     * The first one is the object on which you want to invoke the method.
     * The second one is actually a vararg, so you can pass any number of arguments with this declaration, even no argument.
     * This vararg receives the parameters you need to pass to the method you are invoking. You can get more information on this
     * variable argument feature on this page.
     * If the method you are invoking does not take any parameter, then you need to call the invoke() method with only one
     * argument: the object on which you are invoking this method. On the other hand, if the method you are invoking takes two
     * parameters, then you invocation of the invoke() method needs three arguments: the object on which you are invoking this
     * method, and the two arguments this method takes.
     *
     * Getting the Result of an Invoked Method
     * Let us start with a simple example. Suppose you want to get the length of a string of characters using the Reflection
     * API. The String.length() does not take any parameter, which make the invocation of this method easier. All you need to
     * do is to call the invoke() method of the Method class, passing it the string on which you want to invoke the length()
     * method.
     *
     * String hello = "Hello";
     *
     * Class<?> stringClass = String.class;
     * Method lengthMethod = stringClass.getMethod("length");
     *
     * int length = (int)lengthMethod.invoke(hello);
     * System.out.println("length = " + length);
     * Note that the getMethod() method returns a type Object, meaning that you need to cast the return object to the correct
     * type, if you know it. Running this code prints the following:
     *
     * length = 5
     * Passing Parameters to the Invoked Method
     * The objects and values you need to pass to the method you invoke can be passed to invoke() as further arguments. In the
     * following example, we are calling the substring() method, that takes two integers. Note that there are two overloads of
     * the String.substring() method, one that takes one int, and the other that takes two. Here we are getting the second
     * overload, by passing the two int.class arguments.
     *
     * String hello = "Hello world!";
     * Class<?> stringClass = String.class;
     *
     * Method substringMethod = stringClass.getMethod("substring", int.class, int.class);
     * String substring = (String)substringMethod.invoke(hello, 6, 11);
     * System.out.println("substring = " + substring);
     * The previous code prints the following.
     *
     * substring = world
     * Getting the Thrown Exceptions
     * There are cases where the method you invoke using the Reflection API may throw exceptions that you need to be aware of.
     * The Reflection API catches these exceptions for you, and wrap them in a InvocationTargetException that you can catch
     * and analyze.
     *
     * Suppose you need to open a file for writing using the Reflection API, but pass a file that cannot be opened. You can do
     * that with the following code.
     *
     * Note that we are using the Files.newBufferedWriter() method, that declares a variable argument. To get such a method,
     * you need to consider this variable argument as an array, and pass an empty array of the correct type to the invoke() call.
     *
     * Note also that the Files.newBufferedWriter() is static. So you do not invoke it on any object. In that case, the first
     * argument passed to the invoke() method is null.
     *
     * Path doesNotExist = Path.of("/does/not/exist");
     * Class<?> filesClass = Files.class;
     * Method newBufferedWriterMethod =
     *         filesClass.getMethod("newBufferedWriter", Path.class, OpenOption[].class);
     * try (Writer writer = (Writer)newBufferedWriterMethod.invoke(null, doesNotExist, new OpenOption[0]);) {
     *     writer.write("Hello");
     * } catch (InvocationTargetException ite) {
     *     Throwable targetException = ite.getTargetException();
     *     System.out.println("ite.getTargetException() = " + targetException);
     *     System.out.println("Stack trace:");
     *     targetException.printStackTrace();
     * } catch (IOException e) {
     *     throw new RuntimeException(e);
     * }
     * The opening and the closing of the writer (which is done by the use of this try-with-resources statement) throws an
     * IOException that is caught by the Reflection API, and wrapped in a InvocationTargetException.
     * The InvocationTargetException class has a getTargetException() method that gives you this exception. We are using this
     * pattern in this example. Running it will give you the following.
     *
     * ite.getTargetException() = java.nio.file.NoSuchFileException: \does\not\exist
     * Stack trace:
     * java.nio.file.NoSuchFileException: \does\not\exist
     *     // this part of the stack trace depends on your file system
     *     at java.base/java.nio.file.spi.FileSystemProvider.newOutputStream(FileSystemProvider.java:484)
     *     at java.base/java.nio.file.Files.newOutputStream(Files.java:228)
     *     at java.base/java.nio.file.Files.newBufferedWriter(Files.java:3007)
     *     at java.base/java.nio.file.Files.newBufferedWriter(Files.java:3055)
     *     at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
     *     at java.base/java.lang.reflect.Method.invoke(Method.java:578)
     *     at org.devjava.Main.main(Main.java:48)
     */

    /**
     * Accessing Private Methods
     * Trying to invoke a private method from outside the class, or any other case of a non-visible method will throw a
     * IllegalAccessException.
     *
     * You can still get access to such a method and invoke it, by first calling method.setAccessible(true) on it. This call
     * may fail in case you are trying to invoke a method from a class in another module, due to the restrictions on the class
     * members a module. You can check the page Reflective Access with Open Modules and Open Packages for more information.
     *
     * Note that calling setAccessible(true) on a method does not make this method public. It just suppresses the checks for
     * access control.
     */

    /**
     * Generics and Type Erasure
     * You need to keep in mind that the discovery of a method depends on how this method has been compiled. The type parameter
     * of a method is erased at compile time, meaning that you cannot use it with the Reflection API.
     *
     * Consider the following example, where you want to access the add() method of a List object.
     *
     * List<String> strings = new ArrayList<>();
     *
     * Class<?> stringsClass = strings.getClass();
     * Method addString = stringsClass.getMethod("add", String.class);
     * Here you are trying to get a reference on the add() method of the class of an object of type List<String>. This method
     * declares a String parameter. But because this parameter has in fact the type E (that receives the value String in this
     * example), it is erased, and has in fact the type Object.
     *
     * So running this code produces the following result, because there is no add() method on this class, that takes a String
     * as a parameter.
     *
     * Exception in thread "main" java.lang.NoSuchMethodException: java.util.ArrayList.add(java.lang.String)
     *     at java.base/java.lang.Class.getMethod(Class.java:2277)
     *     at org.devjava.Main.main(Main.java:47)
     * The correct code is the following, that looks for a method that declares a parameter of type Object:
     *
     * List<String> strings = new ArrayList<>();
     *
     * Class<?> stringsClass = strings.getClass();
     * Method addString = stringsClass.getMethod("add", Object.class);
     * You can then invoke this method, passing a string to it. But by doing so you loose the type safety of calling the
     * method directly.
     */
}
